#include <zenoui/check_gl.hpp>
#include <zenox/minilog.hpp>
#include <zenox/pragma.hpp>
#include <stdexcept>
#include <iostream>
#include <sstream>
#include <cstring>
#include <string>
#include <set>

namespace zenoui {

const char *check_gl::opengl_errno_name(GLenum err) {
    switch (err) {
#define PER_GL_ERROR(x) case GL_##x: return #x;
    PER_GL_ERROR(NO_ERROR)
    PER_GL_ERROR(INVALID_ENUM)
    PER_GL_ERROR(INVALID_VALUE)
    PER_GL_ERROR(INVALID_OPERATION)
    PER_GL_ERROR(STACK_OVERFLOW)
    PER_GL_ERROR(STACK_UNDERFLOW)
    PER_GL_ERROR(OUT_OF_MEMORY)
#undef PER_GL_ERROR
    }
    return "unknown error";
}

#ifndef NDEBUG
ZENO_HOT_FN
#endif
void check_gl::opengl_check_error(const char *filename, int lineno, const char *expr) {
    GLenum err = glad_glGetError();
    if (ZENO_UNLIKELY(err != GL_NO_ERROR)) [[unlikely]] {
        zenox::fatal_error("{}:{}: {} failed: {}", filename, lineno, expr, opengl_errno_name(err));
        ZENO_UNREACHABLE();
    }
}

bool check_gl::opengl_has_extension(const char *extension) {
    static auto exts = [] {
        GLint n = 0;
        glGetIntegerv(GL_NUM_EXTENSIONS, &n);
        std::set<std::string> res;
        for (GLint i = 0; i < n; i++) {
            const GLubyte *ext = glGetStringi(GL_EXTENSIONS, i);
            res.emplace(reinterpret_cast<const char *>(ext));
        }
        return res;
    }();
    return exts.find(extension) != exts.end();
}

static void APIENTRY opengl_debug_message_callback(
    GLenum source, GLenum type, GLuint id,
    GLenum severity, GLsizei length,
    const GLchar *msg, const void *data)
{
    (void)length;
    (void)data;

    if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;

    const char *_source = "???";
    const char *_type = "???";
    const char *_severity = "???";

    switch (source) {
#define PER_GL_DEBUG_SOURCE(x) case GL_DEBUG_SOURCE_##x: _source = #x; break;
    PER_GL_DEBUG_SOURCE(API)
    PER_GL_DEBUG_SOURCE(WINDOW_SYSTEM)
    PER_GL_DEBUG_SOURCE(SHADER_COMPILER)
    PER_GL_DEBUG_SOURCE(THIRD_PARTY)
    PER_GL_DEBUG_SOURCE(APPLICATION)
    PER_GL_DEBUG_SOURCE(OTHER)
#undef PER_GL_DEBUG_SOURCE
    };

    switch (type) {
#define PER_GL_DEBUG_TYPE(x) case GL_DEBUG_TYPE_##x: _type = #x; break;
    PER_GL_DEBUG_TYPE(ERROR)
    PER_GL_DEBUG_TYPE(UNDEFINED_BEHAVIOR)
    PER_GL_DEBUG_TYPE(PORTABILITY)
    PER_GL_DEBUG_TYPE(PERFORMANCE)
    PER_GL_DEBUG_TYPE(OTHER)
    PER_GL_DEBUG_TYPE(MARKER)
#undef PER_GL_DEBUG_TYPE
    }

    switch (severity) {
#define PER_GL_DEBUG_SEVERITY(x) case GL_DEBUG_SEVERITY_##x: _severity = #x; break;
    PER_GL_DEBUG_SEVERITY(HIGH)
    PER_GL_DEBUG_SEVERITY(MEDIUM)
    PER_GL_DEBUG_SEVERITY(LOW)
    PER_GL_DEBUG_SEVERITY(NOTIFICATION)
#undef PER_GL_DEBUG_SEVERITY
    }

    zenox::log_level _level = zenox::log_level::info;
    switch (severity) {
#define PER_GL_DEBUG_SEVERITY(x, y) case GL_DEBUG_SEVERITY_##x: _level = zenox::log_level::y; break;
    PER_GL_DEBUG_SEVERITY(HIGH, error)
    PER_GL_DEBUG_SEVERITY(MEDIUM, warn)
    PER_GL_DEBUG_SEVERITY(LOW, info)
    PER_GL_DEBUG_SEVERITY(NOTIFICATION, trace)
    }

    zenox::format_log(_level, "OpenGL error [{}]: {} of {} severity, raised from {}: {}", id, _type, _severity, _source, msg);
}

void check_gl::opengl_try_enable_debug_message() {
    if (opengl_has_extension("GL_ARB_debug_output")) {
        auto my_glDebugMessageCallbackARB = (PFNGLDEBUGMESSAGECALLBACKPROC)glfwGetProcAddress("glDebugMessageCallbackARB");
        if (my_glDebugMessageCallbackARB) {
            my_glDebugMessageCallbackARB(opengl_debug_message_callback, nullptr);
            glEnable(GL_DEBUG_OUTPUT);
        }
    }
}

ZENO_COLD_FN void check_gl::opengl_show_glfw_error_diagnose(const char *message) {
    const char *errmsg;
    glfwGetError(&errmsg);
    if (!errmsg) errmsg = "(no error)";
    std::ostringstream oss;
    oss << "GLFW failed in " << message << ": " << errmsg << '\n';
    oss << "==============================================\n";
    if (!strcmp(errmsg, "X11: The DISPLAY environment variable is missing") || !strcmp(errmsg, "X11: Failed to open display")) {
        oss << "** You seems not running with graphic display\n";
        oss << "** 您似乎没有运行图形显示\n";
    } else if (!strcmp(errmsg, "WGL: The driver does not appear to support OpenGL")) {
        oss << "** Please consider install an OpenGL driver, or use the mesa driver\n";
        oss << "** 请考虑安装OpenGL驱动程序，或使用mesa驱动\n";
    } else if (!strcmp(errmsg, "WGL: Failed to create OpenGL context")) {
        oss << "** Your driver seems not supporting the required OpenGL version\n";
        oss << "** 您的驱动程序似乎不支持所需的OpenGL版本\n";
    }
    oss << "- If you have a physical graphic card (e.g. NVIDIA), install it from your graphic card vendor official website: http://www.nvidia.com/Download/index.aspx\n";
    oss << "- If you are using Windows, download opengl32.dll from https://pan.baidu.com/s/1TZ6nVJC7DZIuUarZrGJYow?pwd=opgl and place it into the same directory as this executable file (alternatively you may download opengl32sw.dll from Internet and rename it to opengl32.dll to place into the same directory as this executable file)\n";
    oss << "- If you are using Linux server or WSL1, please install the mesa driver via `apt install libglapi-mesa` or follow this guide: https://ubuntuhandbook.org/index.php/2021/07/install-latest-mesa-ubuntu-20-04-21-04/";
    oss << "- If you use WSL2, please install WSLg: https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/gui-apps\n";
    oss << "- If you are using SSH connection for remote server, try connect it using ssh -X <ip address>\n";
    oss << "- If you are using MacOS, you probably want to use Windows or Linux instead for better OpenGL support\n";
    oss << "- If you are using a Laptop with dual-cards, make sure you have switch to dedicated card (NVIDIA) instead of the integrated card (Intel)\n";
    oss << "----------------------------------------------\n";
    oss << "- 如果您有一张物理图形卡（例如NVIDIA），请从图形卡供应商的官方网站下载并安装驱动程序：http://www.nvidia.com/Download/index.aspx\n";
    oss << "- 如果您使用的是Windows操作系统，请从https://pan.baidu.com/s/1TZ6nVJC7DZIuUarZrGJYow?pwd=opgl下载opengl32.dll并将其放置在与可执行文件相同的目录中（或者您可以从互联网上下载opengl32sw.dll并将其重命名为opengl32.dll，然后放置在与可执行文件相同的目录中）\n";
    oss << "- 如果您使用的是Linux服务器或WSL1，请通过`apt install libglapi-mesa`安装mesa驱动程序，或者按照该指南操作：https://ubuntuhandbook.org/index.php/2021/07/install-latest-mesa-ubuntu-20-04-21-04/\n";
    oss << "- 如果您使用的是WSL2，请安装WSLg：https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/gui-apps\n";
    oss << "- 如果您使用SSH连接远程服务器，请尝试使用ssh -X <ip地址>进行连接\n";
    oss << "- 如果您使用的是MacOS，您可能会希望改用Windows或Linux以获得更好的OpenGL支持\n";
    oss << "- 如果您使用的是双显卡的笔记本电脑，请确保已切换到独立显卡（NVIDIA）而不是集成显卡（Intel）\n";
    oss << "==============================================";
    zenox::fatal_error("{}", oss.str());
}

}
